# ejs模板引擎
30 秒速记
<%= %>转义输出(默认用它)、<%- %>原样输出(只给可信HTML)、<% %>写JS逻辑- 复用片段:
<%- include('header', { title }) %>,EJS 3删掉了旧的<% include header %>写法 - 传给模板的数据要白名单,别把
ctx.query整个塞进去 - 模板只管展示,不在里面查库、做权限判断
- 转义只解决
HTML内容里的XSS,href="javascript:..."、内联<script>里的数据还要单独处理
EJS 就是「在 HTML 里写 JS」:用 <% %> 写逻辑,用 <%= %> 输出变量。 最该记住的是两种输出标签的区别:<%= %> 会转义,用户内容默认都用它;<%- %> 原样输出,只用来输出 include 进来的片段或者你自己生成的可信 HTML。公共的头部、底部用 include 复用,注意 EJS 3 只支持函数写法。另外转义不是万能的,用户输入的链接放进 href 时,要校验协议,防 javascript: 这种。
理解「ejs模板引擎」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 module 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
EJS 的标签不多,一张表就能记全:
| 标签 | 作用 |
|---|---|
<% code %> |
执行 JS,不输出 |
<%= value %> |
输出并做 HTML 转义 |
<%- value %> |
原样输出,不转义 |
<%# comment %> |
注释,不出现在结果里 |
-%> / <%_ _%> |
去掉标签前后的换行或空白 |
一个典型的列表页模板:
<%- include('partials/header', { title }) %>
<ul>
<% if (list.length === 0) { %>
<li>暂无数据</li>
<% } %>
<% list.forEach(item => { %>
<li><%= item.name %></li> <!-- 用户内容,转义输出 -->
<% }) %>
</ul>
<%- include('partials/footer') %>
可以直接在 Node 里试转义效果:
const ejs = require('ejs')
console.log(ejs.render('<p><%= s %></p>', { s: '<b>hi</b>' }))
// <p><b>hi</b></p>
console.log(ejs.render('<p><%- s %></p>', { s: '<b>hi</b>' }))
// <p><b>hi</b></p>
工程上两条底线:传给 render 的数据只放明确需要的字段,EJS 低于 3.1.7 的版本存在通过模板参数远程执行代码的漏洞,老项目要升级;<%- %> 只用于 include 和可信内容,代码评审时看到它输出用户数据就要拦。
面试官追问
追问 1评论区用 <%- comment %> 输出,为了保留用户的换行和链接,有什么问题?
这就是存储型 XSS,用户提交一段 <script> 所有访客都会执行。要保留格式就先转义,再自己把换行替换成 <br>;需要富文本就用 DOMPurify 这类库做白名单过滤后再原样输出。
追问 2<%= %> 已经转义了,<a href="<%= url %>"> 是不是就安全了?
不安全,javascript:alert(1) 里没有需要转义的字符,点了照样执行。链接要校验只允许 http: / https: 开头。
追问 3升级到 EJS 3 后,<% include header %> 报错了,怎么改?
EJS 3 删掉了这种指令写法,改成 <%- include('header') %>。要传数据就写 <%- include('header', { title }) %>,注意用 <%-,用 <%= 会把片段 HTML 转义成文字。
追问 4模板里能直接 require 数据库查用户列表吗?
技术上能写 JS,但不该这么干。模板里查库会让渲染变慢、错误难追、逻辑没法测。数据在中间件里查好,模板只负责把它摆出来。
# ejs模板引擎
# 具体查看ejs官方文档
30 秒速记
- 官方仓库
github.com/mde/ejs,语法、选项、CLI都以它的README为准 - 常用选项:
cache(缓存编译结果)、delimiter(换掉%)、root/views(include的查找目录) - 网上很多教程还是
EJS 2的include写法,EJS 3只支持include()函数 - 版本至少
3.1.10,老版本有远程代码执行和原型污染相关的安全问题
EJS 的细节直接看官方仓库 mde/ejs 的 README 就够了,文档不长,比二手教程靠谱。 重点看三块:标签语法、render / renderFile 的选项、include 的路径规则。二手教程最容易过时的地方是 include,很多还在用 EJS 2 的 <% include xxx %>,EJS 3 已经删了。另外 EJS 修过好几个安全漏洞,3.1.7 修了模板参数导致的远程代码执行,之后的版本又补了原型污染相关的问题,老项目我会先看一眼版本号。
理解「具体查看ejs官方文档」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 module 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
EJS 官方 README 里最常用的几个 API:
const ejs = require('ejs')
// 1. 渲染字符串模板
ejs.render('<h1><%= title %></h1>', { title: 'hi' })
// '<h1>hi</h1>'
// 2. 先编译再多次渲染,适合同一模板反复用
const tpl = ejs.compile('<li><%= name %></li>')
tpl({ name: 'a' }) // '<li>a</li>'
tpl({ name: 'b' }) // '<li>b</li>'
// 3. 渲染文件,不传回调时返回 Promise
const html = await ejs.renderFile('./view/index.ejs', { title: 'hi' }, {
cache: process.env.NODE_ENV === 'production', // 生产缓存编译结果
rmWhitespace: false
})
render 的第三个参数是选项,第二个参数是数据,这两个千万别混。早期版本里数据对象里的某些字段会被当成选项读取,这正是 3.1.7 之前远程代码执行漏洞的根源:攻击者通过查询参数注入 outputFunctionName 这类选项,把任意代码拼进编译后的函数。所以无论用哪个版本,都只往数据参数里放你明确列出的字段。
查文档时还要留意 include 的两种路径:相对路径相对当前模板文件,绝对路径相对 root / views 选项。用 ejs.render 渲染字符串时没有「当前文件」,要么传 filename,要么用绝对路径配 views。
面试官追问
追问 1想在 Node 脚本里直接渲染一个模板文件生成静态 HTML,用哪个 API?
ejs.renderFile('view/index.ejs', data),不传回调时返回 Promise,await 拿到字符串后写文件就行。
追问 2模板里的 <% 和另一个模板语法冲突了,能换分隔符吗?
能,传 delimiter: '?' 之后标签就变成 <?= ?>。EJS 3 还能用 openDelimiter / closeDelimiter 把尖括号也换掉。
追问 3include('header') 报找不到文件,查找路径是怎么算的?
相对路径按当前模板文件所在目录找;以 / 开头的绝对路径,会去 root 或 views 选项指定的目录找。渲染字符串模板时没有当前文件,要传 filename 选项,否则相对 include 用不了。
追问 4npm audit 报 ejs 有严重漏洞,项目里没用户传模板,能不升吗?
别赌。那个漏洞的触发条件是把用户可控对象传进 render 的数据参数,很多项目图省事传了 ctx.query 或 req.body 都中招。升到 3.1.10 以上是小版本升级,基本没有兼容问题。
# 可运行示例:区分转义输出与可信 HTML
30 秒速记
<%= %>会做HTML转义,<%- %>原样输出 → 用户内容一律用<%=<%-只留给自己能担保的内容:模板
